/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.actionbarsherlock.internal.view.menu;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.Context;
import android.content.res.XmlResourceParser;
import android.support.v4.view.Menu;
import android.support.v4.view.MenuItem;
import android.support.v4.view.SubMenu;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import android.view.InflateException;
import android.view.View;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * This class is used to instantiate menu XML files into Menu objects.
 * <p>
 * For performance reasons, menu inflation relies heavily on pre-processing of
 * XML files that is done at build time. Therefore, it is not currently possible
 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
 * it only works with an XmlPullParser returned from a compiled resource (R.
 * <em>something</em> file.)
 */
public class MenuInflaterImpl extends android.view.MenuInflater {
	private static final String LOG_TAG = "MenuInflater";
	private static final String XML_NS = "http://schemas.android.com/apk/res/android";

	/*
	 * This doesn't work reliably and I can't yet figure out why.
	 * 
	 * 
	 * private static final int[] MenuGroup = new int[] { //Ascending order by
	 * value android.R.attr.enabled, android.R.attr.id, android.R.attr.visible,
	 * android.R.attr.menuCategory, android.R.attr.orderInCategory,
	 * android.R.attr.checkableBehavior, }; private static final int
	 * MenuGroup_enabled = 0; private static final int MenuGroup_id = 1; private
	 * static final int MenuGroup_visible = 2; private static final int
	 * MenuGroup_menuCategory = 3; private static final int
	 * MenuGroup_orderInCategory = 4; private static final int
	 * MenuGroup_checkableBehavior = 5;
	 * 
	 * 
	 * private static final int[] MenuItem = new int[] { //Ascending order by
	 * value android.R.attr.icon, android.R.attr.enabled, android.R.attr.id,
	 * android.R.attr.checked, android.R.attr.visible,
	 * android.R.attr.menuCategory, android.R.attr.orderInCategory,
	 * android.R.attr.title, android.R.attr.titleCondensed,
	 * android.R.attr.alphabeticShortcut, android.R.attr.numericShortcut,
	 * android.R.attr.checkable, android.R.attr.onClick,
	 * android.R.attr.showAsAction, android.R.attr.actionLayout,
	 * android.R.attr.actionViewClass, }; private static final int MenuItem_icon
	 * = 0; private static final int MenuItem_enabled = 1; private static final
	 * int MenuItem_id = 2; private static final int MenuItem_checked = 3;
	 * private static final int MenuItem_visible = 4; private static final int
	 * MenuItem_menuCategory = 5; private static final int
	 * MenuItem_orderInCategory = 6; private static final int MenuItem_title =
	 * 7; private static final int MenuItem_titleCondensed = 8; private static
	 * final int MenuItem_alphabeticShortcut = 9; private static final int
	 * MenuItem_numericShortcut = 10; private static final int
	 * MenuItem_checkable = 11; private static final int MenuItem_onClick = 12;
	 * private static final int MenuItem_showAsAction = 13; private static final
	 * int MenuItem_actionLayout = 14; private static final int
	 * MenuItem_actionViewClass = 15;
	 */

	/** Menu tag name in XML. */
	private static final String XML_MENU = "menu";

	/** Group tag name in XML. */
	private static final String XML_GROUP = "group";

	/** Item tag name in XML. */
	private static final String XML_ITEM = "item";

	private static final int NO_ID = 0;

	private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] { Context.class };

	private final Object[] mActionViewConstructorArguments;

	private Context mContext;

	/** Native inflater for context menu fallback. */
	private final android.view.MenuInflater mNativeMenuInflater;

	/**
	 * Constructs a menu inflater.
	 * 
	 * @see Activity#getMenuInflater()
	 */
	public MenuInflaterImpl(Context context, android.view.MenuInflater nativeMenuInflater) {
		super(context);
		mContext = context;
		mActionViewConstructorArguments = new Object[] { context };
		mNativeMenuInflater = nativeMenuInflater;
	}

	/**
	 * Inflate a menu hierarchy from the specified XML resource. Throws
	 * {@link InflateException} if there is an error.
	 * 
	 * @param menuRes
	 *            Resource ID for an XML layout resource to load (e.g.,
	 *            <code>R.menu.main_activity</code>)
	 * @param menu
	 *            The Menu to inflate into. The items and submenus will be added
	 *            to this Menu.
	 */
	public void inflate(int menuRes, android.view.Menu menu) {
		if (!(menu instanceof MenuBuilder)) {
			mNativeMenuInflater.inflate(menuRes, menu);
			return;
		}

		MenuBuilder actionBarMenu = (MenuBuilder) menu;
		XmlResourceParser parser = null;
		try {
			parser = mContext.getResources().getLayout(menuRes);
			AttributeSet attrs = Xml.asAttributeSet(parser);

			parseMenu(parser, attrs, actionBarMenu);
		} catch (XmlPullParserException e) {
			throw new InflateException("Error inflating menu XML", e);
		} catch (IOException e) {
			throw new InflateException("Error inflating menu XML", e);
		} finally {
			if (parser != null)
				parser.close();
		}
	}

	/**
	 * Called internally to fill the given menu. If a sub menu is seen, it will
	 * call this recursively.
	 */
	private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) throws XmlPullParserException, IOException {
		MenuState menuState = new MenuState(menu);

		int eventType = parser.getEventType();
		String tagName;
		boolean lookingForEndOfUnknownTag = false;
		String unknownTagName = null;

		// This loop will skip to the menu start tag
		do {
			if (eventType == XmlPullParser.START_TAG) {
				tagName = parser.getName();
				if (tagName.equals(XML_MENU)) {
					// Go to next tag
					eventType = parser.next();
					break;
				}

				throw new RuntimeException("Expecting menu, got " + tagName);
			}
			eventType = parser.next();
		} while (eventType != XmlPullParser.END_DOCUMENT);

		boolean reachedEndOfMenu = false;
		while (!reachedEndOfMenu) {
			switch (eventType) {
			case XmlPullParser.START_TAG:
				if (lookingForEndOfUnknownTag) {
					break;
				}

				tagName = parser.getName();
				if (tagName.equals(XML_GROUP)) {
					menuState.readGroup(attrs);
				} else if (tagName.equals(XML_ITEM)) {
					menuState.readItem(attrs);
				} else if (tagName.equals(XML_MENU)) {
					// A menu start tag denotes a submenu for an item
					SubMenu subMenu = menuState.addSubMenuItem();

					// Parse the submenu into returned SubMenu
					parseMenu(parser, attrs, subMenu);
				} else {
					lookingForEndOfUnknownTag = true;
					unknownTagName = tagName;
				}
				break;

			case XmlPullParser.END_TAG:
				tagName = parser.getName();
				if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
					lookingForEndOfUnknownTag = false;
					unknownTagName = null;
				} else if (tagName.equals(XML_GROUP)) {
					menuState.resetGroup();
				} else if (tagName.equals(XML_ITEM)) {
					// Add the item if it hasn't been added (if the item was
					// a submenu, it would have been added already)
					if (!menuState.hasAddedItem()) {
						menuState.addItem();
					}
				} else if (tagName.equals(XML_MENU)) {
					reachedEndOfMenu = true;
				}
				break;

			case XmlPullParser.END_DOCUMENT:
				throw new RuntimeException("Unexpected end of document");
			}

			eventType = parser.next();
		}
	}

	private static class InflatedOnMenuItemClickListener extends MenuItem.OnMenuItemClickListener {
		private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };

		private Context mContext;
		private Method mMethod;

		public InflatedOnMenuItemClickListener(Context context, String methodName) {
			mContext = context;
			Class<?> c = context.getClass();
			try {
				mMethod = c.getMethod(methodName, PARAM_TYPES);
			} catch (Exception e) {
				InflateException ex = new InflateException("Couldn't resolve menu item onClick handler " + methodName + " in class " + c.getName());
				ex.initCause(e);
				throw ex;
			}
		}

		public boolean onMenuItemClick(MenuItem item) {
			try {
				if (mMethod.getReturnType() == Boolean.TYPE) {
					return (Boolean) mMethod.invoke(mContext, item);
				} else {
					mMethod.invoke(mContext, item);
					return true;
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * State for the current menu.
	 * <p>
	 * Groups can not be nested unless there is another menu (which will have
	 * its state class).
	 */
	private class MenuState {
		private Menu menu;

		/*
		 * Group state is set on items as they are added, allowing an item to
		 * override its group state. (As opposed to set on items at the group
		 * end tag.)
		 */
		private int groupId;
		private int groupCategory;
		private int groupOrder;
		private int groupCheckable;
		private boolean groupVisible;
		private boolean groupEnabled;

		private boolean itemAdded;
		private int itemId;
		private int itemCategoryOrder;
		private CharSequence itemTitle;
		private CharSequence itemTitleCondensed;
		private int itemIconResId;
		private char itemAlphabeticShortcut;
		private char itemNumericShortcut;
		/**
		 * Sync to attrs.xml enum: - 0: none - 1: all - 2: exclusive
		 */
		private int itemCheckable;
		private boolean itemChecked;
		private boolean itemVisible;
		private boolean itemEnabled;

		/**
		 * Sync to attrs.xml enum, values in MenuItem: - 0: never - 1: ifRoom -
		 * 2: always - -1: Safe sentinel for "no value".
		 */
		private int itemShowAsAction;

		private int itemActionViewLayout;
		private String itemActionViewClassName;

		private String itemListenerMethodName;

		private static final int defaultGroupId = NO_ID;
		private static final int defaultItemId = NO_ID;
		private static final int defaultItemCategory = 0;
		private static final int defaultItemOrder = 0;
		private static final int defaultItemCheckable = 0;
		private static final boolean defaultItemChecked = false;
		private static final boolean defaultItemVisible = true;
		private static final boolean defaultItemEnabled = true;

		public MenuState(final Menu menu) {
			this.menu = menu;

			resetGroup();
		}

		public void resetGroup() {
			groupId = defaultGroupId;
			groupCategory = defaultItemCategory;
			groupOrder = defaultItemOrder;
			groupCheckable = defaultItemCheckable;
			groupVisible = defaultItemVisible;
			groupEnabled = defaultItemEnabled;
		}

		/**
		 * Called when the parser is pointing to a group tag.
		 */
		public void readGroup(AttributeSet attrs) {
			// TypedArray a = mContext.obtainStyledAttributes(attrs,
			// /*com.android.internal.R.styleable.*/MenuGroup);

			// groupId =
			// a.getResourceId(/*com.android.internal.R.styleable.*/MenuGroup_id,
			// defaultGroupId);
			groupId = attrs.getAttributeResourceValue(XML_NS, "id", defaultGroupId);
			// groupCategory =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuGroup_menuCategory,
			// defaultItemCategory);
			groupCategory = attrs.getAttributeIntValue(XML_NS, "menuCategory", defaultItemCategory);
			// groupOrder =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuGroup_orderInCategory,
			// defaultItemOrder);
			groupOrder = attrs.getAttributeIntValue(XML_NS, "orderInCategory", defaultItemOrder);
			// groupCheckable =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuGroup_checkableBehavior,
			// defaultItemCheckable);
			groupCheckable = attrs.getAttributeIntValue(XML_NS, "checkableBehavior", defaultItemCheckable);
			// groupVisible =
			// a.getBoolean(/*com.android.internal.R.styleable.*/MenuGroup_visible,
			// defaultItemVisible);
			groupVisible = attrs.getAttributeBooleanValue(XML_NS, "visible", defaultItemVisible);
			// groupEnabled =
			// a.getBoolean(/*com.android.internal.R.styleable.*/MenuGroup_enabled,
			// defaultItemEnabled);
			groupEnabled = attrs.getAttributeBooleanValue(XML_NS, "enabled", defaultItemEnabled);

			// a.recycle();
		}

		/**
		 * Called when the parser is pointing to an item tag.
		 */
		public void readItem(AttributeSet attrs) {
			// TypedArray a = mContext.obtainStyledAttributes(attrs,
			// /*com.android.internal.R.styleable.*/MenuItem);

			// Inherit attributes from the group as default value
			// itemId =
			// a.getResourceId(/*com.android.internal.R.styleable.*/MenuItem_id,
			// defaultItemId);
			itemId = attrs.getAttributeResourceValue(XML_NS, "id", defaultItemId);
			// final int category =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuItem_menuCategory,
			// groupCategory);
			final int category = attrs.getAttributeIntValue(XML_NS, "menuCategory", groupCategory);
			// final int order =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuItem_orderInCategory,
			// groupOrder);
			final int order = attrs.getAttributeIntValue(XML_NS, "orderInCategory", groupOrder);
			itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
			// itemTitle =
			// a.getText(/*com.android.internal.R.styleable.*/MenuItem_title);
			final int itemTitleId = attrs.getAttributeResourceValue(XML_NS, "title", 0);
			if (itemTitleId != 0) {
				itemTitle = mContext.getString(itemTitleId);
			} else {
				itemTitle = attrs.getAttributeValue(XML_NS, "title");
			}
			// itemTitleCondensed =
			// a.getText(/*com.android.internal.R.styleable.*/MenuItem_titleCondensed);
			final int itemTitleCondensedId = attrs.getAttributeResourceValue(XML_NS, "titleCondensed", 0);
			if (itemTitleCondensedId != 0) {
				itemTitleCondensed = mContext.getString(itemTitleCondensedId);
			} else {
				itemTitleCondensed = attrs.getAttributeValue(XML_NS, "titleCondensed");
			}
			// itemIconResId =
			// a.getResourceId(/*com.android.internal.R.styleable.*/MenuItem_icon,
			// 0);
			itemIconResId = attrs.getAttributeResourceValue(XML_NS, "icon", 0);
			// itemAlphabeticShortcut =
			// getShortcut(a.getString(/*com.android.internal.R.styleable.*/MenuItem_alphabeticShortcut));
			itemAlphabeticShortcut = getShortcut(attrs.getAttributeValue(XML_NS, "alphabeticShortcut"));
			// itemNumericShortcut =
			// getShortcut(a.getString(/*com.android.internal.R.styleable.*/MenuItem_numericShortcut));
			itemNumericShortcut = getShortcut(attrs.getAttributeValue(XML_NS, "numericShortcut"));
			// if
			// (a.hasValue(/*com.android.internal.R.styleable.*/MenuItem_checkable))
			// {
			if (attrs.getAttributeValue(XML_NS, "checkable") != null) {
				// Item has attribute checkable, use it
				// itemCheckable =
				// a.getBoolean(/*com.android.internal.R.styleable.*/MenuItem_checkable,
				// false) ? 1 : 0;
				itemCheckable = attrs.getAttributeBooleanValue(XML_NS, "checkable", false) ? 1 : 0;
			} else {
				// Item does not have attribute, use the group's (group can have
				// one more state
				// for checkable that represents the exclusive checkable)
				itemCheckable = groupCheckable;
			}
			// itemChecked =
			// a.getBoolean(/*com.android.internal.R.styleable.*/MenuItem_checked,
			// defaultItemChecked);
			itemChecked = attrs.getAttributeBooleanValue(XML_NS, "checked", defaultItemChecked);
			// itemVisible =
			// a.getBoolean(/*com.android.internal.R.styleable.*/MenuItem_visible,
			// groupVisible);
			itemVisible = attrs.getAttributeBooleanValue(XML_NS, "visible", groupVisible);
			// itemEnabled =
			// a.getBoolean(/*com.android.internal.R.styleable.*/MenuItem_enabled,
			// groupEnabled);
			itemEnabled = attrs.getAttributeBooleanValue(XML_NS, "enabled", groupEnabled);
			// itemShowAsAction =
			// a.getInt(/*com.android.internal.R.styleable.*/MenuItem_showAsAction,
			// -1);
			itemShowAsAction = attrs.getAttributeIntValue(XML_NS, "showAsAction", -1);
			// itemListenerMethodName =
			// a.getString(/*com.android.internal.R.styleable.*/MenuItem_onClick);
			itemListenerMethodName = attrs.getAttributeValue(XML_NS, "onClick");
			// itemActionViewLayout =
			// a.getResourceId(/*com.android.internal.R.styleable.*/MenuItem_actionLayout,
			// 0);
			itemActionViewLayout = attrs.getAttributeResourceValue(XML_NS, "actionLayout", 0);
			// itemActionViewClassName =
			// a.getString(/*com.android.internal.R.styleable.*/MenuItem_actionViewClass);
			itemActionViewClassName = attrs.getAttributeValue(XML_NS, "actionViewClass");

			// a.recycle();

			itemAdded = false;
		}

		private char getShortcut(String shortcutString) {
			if (shortcutString == null) {
				return 0;
			} else {
				return shortcutString.charAt(0);
			}
		}

		private void setItem(MenuItem item) {
			item.setChecked(itemChecked).setVisible(itemVisible).setEnabled(itemEnabled).setCheckable(itemCheckable >= 1).setTitleCondensed(itemTitleCondensed)
					.setIcon(itemIconResId).setAlphabeticShortcut(itemAlphabeticShortcut).setNumericShortcut(itemNumericShortcut);

			if (itemShowAsAction >= 0) {
				item.setShowAsAction(itemShowAsAction);
			}

			if (itemListenerMethodName != null) {
				if (mContext.isRestricted()) {
					throw new IllegalStateException("The android:onClick attribute cannot " + "be used within a restricted context");
				}
				item.setOnMenuItemClickListener(new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName));
			}

			if (item instanceof MenuItemImpl) {
				MenuItemImpl impl = (MenuItemImpl) item;
				if (itemCheckable >= 2) {
					impl.setExclusiveCheckable(true);
				}
			}

			boolean actionViewSpecified = false;
			if (itemActionViewClassName != null) {
				View actionView = (View) newInstance(itemActionViewClassName, ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
				item.setActionView(actionView);
				actionViewSpecified = true;
			}
			if (itemActionViewLayout > 0) {
				if (!actionViewSpecified) {
					item.setActionView(itemActionViewLayout);
					actionViewSpecified = true;
				} else {
					Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'." + " Action view already specified.");
				}
			}
		}

		public void addItem() {
			itemAdded = true;
			setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
		}

		public SubMenu addSubMenuItem() {
			itemAdded = true;
			SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
			setItem(subMenu.getItem());
			return subMenu;
		}

		public boolean hasAddedItem() {
			return itemAdded;
		}

		@SuppressWarnings("unchecked")
		private <T> T newInstance(String className, Class<?>[] constructorSignature, Object[] arguments) {
			try {
				Class<?> clazz = mContext.getClassLoader().loadClass(className);
				Constructor<?> constructor = clazz.getConstructor(constructorSignature);
				return (T) constructor.newInstance(arguments);
			} catch (Exception e) {
				Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
			}
			return null;
		}
	}
}
